﻿
var QB = QB || {};
/**webapi访问基路径 */

//const webApiRoot = "https://www.qyrjtech.com/api/";
const webApiRoot = "/api/";
/**tokenKey */
const accessTokenKey = "vue-next-admin:access-token";
const refreshAccessTokenKey = `vue-next-admin:x-access-token`;
const userinfoTokenKey = `vue-next-admin:userInfo`;

const sysSessionIdKey = `vue-next-admin:sys-session-id`;
//使用layui的jquery，3.0后删掉
var $ = layui.$;


/**
 * 对象转FormData
 * @param {object} obj 要转的对象
 * @param {string} paramName 对应服务端参数名称
 * @param formData
 * @returns {formData}
 */

QB.ToFormData = function objectToFormData(obj, paramName, formData) {
    formData = formData || new FormData();
    paramName = paramName || "";

    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            const value = obj[key];
            let fullKey = paramName + (paramName ? "." : "") + key;

            // 检查值是否为对象，并且不是文件，不是日期
            if (value instanceof Object && !(value instanceof File) && !(value instanceof Date)) {
                // 如果值是数组，我们需要为每个元素创建带索引的键名
                if (Array.isArray(value)) {
                    value.forEach((item, index) => {
                        // 对于数组中的每个对象，我们使用带有索引的键名
                        const arrayKey = `${fullKey}[${index}]`;
                        objectToFormData(item, arrayKey, formData);
                    });
                } else {
                    // 对于普通对象，递归调用 objectToFormData 函数
                    objectToFormData(value, fullKey, formData);
                }
            } else {
                // 对于非对象或文件类型的值，直接添加到 formData 中
                formData.append(fullKey, value);
            }
        }
    }

    return formData;
};

/**
 * 将对象转为rul参数
 * @param {object} obj
 * @returns
 */
QB.toQueryString = function (obj) {
    return Object.keys(obj)
        .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(typeof obj[key] === "object"
            ? JSON.stringify(obj[key])
            : obj[key])}`)
        .join("&");
};

/**
 * 将文件名非法字符替换为全角字符
 * @param {string} str 文件名
 * @returns{string}
 */
QB.repIllCharFull = function (str) {
    return str.replace(/[\\/:*?"<>|]/g,
        function (match) {
            return String.fromCharCode(match.charCodeAt(0) + 0xFEE0);
        });
};

/**
 * 设置键盘事件
 * 使用 QB.SetKeyPress('Ctrl-Shift-Key', executeMethod);
 * 或者 QB.SetKeyPress('Ctrl-Shift-Key', executeMethod).then(() => {  console.log('Promise 被解决');});
 * 或者 QB.SetKeyPress('Ctrl-Shift-Key').then(() => {  console.log('Promise 被解决');});
 * @param {string} shortcut 快捷键
 * @param {any} executeMethod 执行方法
 * @returns{Promise}
 */
QB.SetKeyPress = function (shortcut, executeMethod) {
    return new Promise((resolve, reject) => {
        document.addEventListener("keydown",
            function (event) {
                const keys = shortcut.split("-");
                const ctrlKey = keys.includes("Ctrl") || keys.includes("ctrl");
                const shiftKey = keys.includes("Shift") || keys.includes("shift");
                const pressedKey = keys[keys.length - 1];

                if (event.ctrlKey === ctrlKey && event.shiftKey === shiftKey && event.key === pressedKey) {
                    event.preventDefault(); // 阻止默认行为
                    if (executeMethod) {
                        executeMethod(); // 执行传入的方法
                    }
                    resolve(); // 解决 Promise
                }
            });
    });
};

/**
 * 初始化屏幕截图功能。
 * @param {string} [html2canvasURL] - 可选的 html2canvas 库 URL。
 * @param {Function} callback - 屏幕截图完成后的回调函数。
 */
QB.ScreenShot = (html2canvasURL, callback) => {
    // 如果未提供 html2canvasURL，则使用默认URL
    const defurl = html2canvasURL || "/js/html2canvas.min.js";

    // 设置文档的用户选择属性
    const setUserSelect = (value) => {
        ['userSelect', 'mozUserSelect', 'msUserSelect'].forEach(prop => {
            document.body.style[prop] = value; // 设置用户选择样式为指定值
        });
    };

    // 执行截图操作
    const takeScreenshot = (x, y, width, height, e) => {
        window.html2canvas(document.body, { x, y, width, height }).then(canvas => {
            const imageData = canvas.toDataURL("image/png"); // 将canvas转换为图片数据
            if (callback) callback(imageData, e); // 如果存在回调函数，则调用
            else { // 否则，新开窗口显示截图
                // 否则，新开窗口显示截图
                //const newWindow = window.open("screenshot-display.html", "_blank");
                const newWindow = window.open(window.location.protocol + "//" + window.location.host + "/screenshot-display.html", "_blank");
                if (newWindow) {
                    // 解码当前 URL
                    //newWindow.onload = () => {
                    const decodedFileName =
                        decodeURI(window.location.href.replace(window.location.origin, "").substring(1));
                    // 将文件名中的非法字符替换为全角字符
                    const cleanedFileName = QB.repIllCharFull(decodedFileName) + '.png';
                    newWindow["img"] = imageData;
                    newWindow["FileName"] = cleanedFileName;
                    newWindow["url"] = decodeURI(window.location.href);
                    //}
                } else {
                    alert("无法打开新窗口，请检查浏览器弹窗设置。");
                }
            }
        }).catch(err => {
            console.error("Screenshot failed:", err); // 截图失败的错误处理
            alert("截图失败，请重试。"); // 提示用户截图失败
        });
    };

    // 初始化截图功能
    const init = () => {
        let divScreeenShot = document.querySelector(".qyScreenShot");
        if (divScreeenShot) return;
        let isDragging = false; // 标记是否正在拖动
        let startX, startY, endX, endY;; // 记录拖动开始的坐标
        const screenshotBox = document.createElement("div"); // 创建截图框元素
        screenshotBox.className = "qyScreenShot";
        // 设置截图框的样式
        Object.assign(screenshotBox.style, { position: "absolute", border: "3px dashed red", pointerEvents: "none", zIndex: "99999999999", display: "none" });
        document.body.appendChild(screenshotBox); // 将截图框添加到文档中



        // 拖动过程中的事件处理
        const dragging = (e) => {
            if (!isDragging) return;
            [endX, endY] = [e.clientX, e.clientY]; // 获取当前坐标
            const [width, height] = [Math.abs(endX - startX), Math.abs(endY - startY)]; // 计算截图框的宽高
            // 更新截图框的位置和大小
            Object.assign(screenshotBox.style, { width: `${width}px`, height: `${height}px`, left: `${Math.min(startX, endX)}px`, top: `${Math.min(startY, endY)}px` });
        };

        // 结束拖动的事件处理
        const endDrag = (e) => {
            if (!isDragging) return;
            isDragging = false; // 重置拖动状态
            screenshotBox.style.display = "none"; // 隐藏截图框
            // 执行截图
            takeScreenshot(Math.min(startX, endX), Math.min(startY, endY), Math.abs(endX - startX), Math.abs(endY - startY), e);
            setUserSelect(""); // 恢复文档内容可被选择
            document.removeEventListener("mousemove", dragging);
            document.removeEventListener("mouseup", endDrag);
        };
        // 开始拖动的事件处理
        const startDrag = (event) => {
            if (event.target.tagName.toLowerCase() === "input" || event.target.tagName.toLowerCase() === "textarea" || !event.ctrlKey || !event.shiftKey) return;
            [isDragging, startX, startY] = [true, event.clientX, event.clientY]; // 设置拖动状态和起始坐标
            Object.assign(screenshotBox.style, { left: `${startX}px`, top: `${startY}px`, width: "0", height: "0", display: "block" });
            setUserSelect("none"); // 禁止文档内容被选择
            document.addEventListener("mousemove", dragging);
            document.addEventListener("mouseup", endDrag);
        };
        // 添加事件监听器
        document.addEventListener("mousedown", startDrag);
        //document.addEventListener("mousemove", dragging);
        //document.addEventListener("mouseup", endDrag);
    };

    // 检查 html2canvas 是否已加载
    if (typeof window.html2canvas === "undefined") {
        const html2canvasScript = document.createElement("script"); // 创建script元素加载html2canvas
        html2canvasScript.src = defurl;
        document.head.appendChild(html2canvasScript);
        html2canvasScript.onload = init; // 加载完成后初始化截图功能
        html2canvasScript.onerror = () => {
            console.error("Failed to load html2canvas.");
            alert("无法加载截图库，请检查您的网络连接或稍后再试。"); // 加载失败的用户反馈
        };
    } else {
        init(); // 如果html2canvas已加载，直接初始化
    }
};


/**
 * 发起通用请求
 * @param {string} url - 请求的 URL
 * @param {string} method - 请求的方法类型，例如 'GET'、'POST' 等
 * @param {FormData|Function|Object} requestData - 请求参数，可以是 FormData、函数或普通对象
 * @param {boolean} [isAuthenticated=false] - 是否需要进行身份验证，默认为 false
 * @param {string} [responseType='json'] - 预期的响应数据类型，默认为 'json'。
 * @returns {Promise<Object>} - 返回一个 Promise 对象，用于处理请求结果
 */
QB.CreateRequest = function (url, method, requestData, isAuthenticated = false, responseType = "json", pageObj) {
    // 返回一个新的Promise对象
    return new Promise((resolve, reject) => {
        // 如果requestData是函数，则调用函数获取数据，否则直接使用requestData
        const processData = typeof requestData === "function" ? requestData() : requestData;

        // 处理requestData数据
        Promise.resolve(processData)
            .then(data => {
                // 如果是GET请求，并且requestData不为空，还需将requestData序列化为URL参数
                if (method.toUpperCase() === "GET" && data) {
                    url += (url.includes("?") ? "&" : "?") + QB.ToQueryParams(data);
                }
                //const body = data instanceof FormData //requestData是FormData 直接设置
                //    ? data
                //    : method.toUpperCase() === "GET" //请求方式是'GET'，则不设置请求体
                //        ? null
                //        : JSON.stringify(data); //否则将数据JSON化
                var body = null;
                //FormData 通常用于 POST 方法，因为它主要设计用于在请求体中传输键值对（如文件上传和表单数据）
                if (data instanceof FormData) {
                    body = data;
                    method = "POST";
                } else if (method.toUpperCase() === "GET") {
                    body = null;
                } else {
                    const processedData = recursiveStringify(data);
                    function recursiveStringify(obj) {
                        if (typeof obj !== 'object' || obj === null) {
                            return obj; // 如果不是对象或为null，直接返回  
                        }

                        const newObj = Array.isArray(obj) ? [] : {}; // 创建新对象或数组  

                        for (const key in obj) {
                            if (obj.hasOwnProperty(key)) {
                                const value = obj[key];
                                if (value instanceof Date) {
                                    newObj[key] = value.toISOString().replace("Z", ""); // 转换日期  
                                } else {
                                    newObj[key] = recursiveStringify(value); // 递归处理  
                                }
                            }
                        }

                        return newObj;
                    }
                    body = JSON.stringify(processedData);
                }

                // 若需要身份验证，则调用函数获取认证头信息，否则为空对象
                const headers = isAuthenticated ? QB.getHeadersWithTokenAndRefresh(pageObj) : {};

                // 如果请求方式不是'GET'且requestData不是FormData实例，则设置Content-Type为JSON
                if (method.toUpperCase() !== "GET" && !(data instanceof FormData)) {
                    headers["Content-Type"] = "application/json; charset=utf-8";
                }
                //console.log(JSON.stringify({ method, body, headers }));
                // 发起fetch请求
                return fetch(url, { method, headers, body });
            })
            .then(response => {
                // 如果响应状态不是成功的，抛出错误
                if (!response.ok) {
                    //throw new Error(`HTTP error, status = ${response.status}`);
                    resolve({ data: { Type: "error", Message: "HTTP error", Code: response.status } });
                }
                // 根据responseType解析响应数据
                return (response[responseType.toLowerCase()] ? response[responseType.toLowerCase()]() : response.text())
                    .then(data => {
                        if (responseType.toLowerCase() !== "text" && typeof data === "object" && "code" in data && data.code === 401) {
                            let ws = window["QyWebSocket"];
                            if (ws) ws.close(1000, '正常关闭');
                            QB.LoginOut();
                        }


                        QB.RefreshToken(response);
                        // 解析成功后，使用resolve函数解决Promise，并传递数据和原始响应对象
                        resolve({ data, response });
                    }).catch(error => {
                        console.error("请求返回数据错误: ", error);
                        reject(error);
                    });
            })
            .catch(error => {
                // 请求过程中捕获到错误时，使用reject函数拒绝Promise，并记录错误信息
                console.error("请求处理失败: ", error);
                reject(error);
            });
    });
};

/**
 * 将复杂对象转换为URL查询字符串。【注意：如果含有数组，需要和后端联调，有的后端不接收索引的方式】
 * @param {Object} object 要被序列化为查询字符串的复杂对象。
 * @param {string} [parentKey=""] 序列化时用于嵌套属性的前缀。
 * @returns {string} 序列化结果，可以直接作为URL的查询部分使用。
 */
QB.ToQueryParams = function (object, parentKey = "") {
    const queryString = [];

    // 内联处理函数，将对象的当前层级序列化为查询字符串
    function serialize(obj, currentPrefix) {
        Object.keys(obj).forEach(key => {
            // 处理键的编码以及嵌套的前缀
            const encodedKey = encodeURIComponent(key);
            const fullKey = currentPrefix ? `${currentPrefix}[${encodedKey}]` : encodedKey;

            const value = obj[key];
            if (typeof value === "object" && value !== null) {
                // 递归对嵌套对象或数组进行序列化
                if (Array.isArray(value)) {
                    value.forEach((item, index) => {
                        queryString.push(`${fullKey}[${index}]=${encodeURIComponent(item)}`);
                    });
                } else if (value instanceof Date) {
                    // 对于Date对象，将其转换为ISOString格式
                    queryString.push(`${fullKey}=${encodeURIComponent(value.toISOString())}`);
                } else {
                    // 继续递归处理对象
                    serialize(value, fullKey);
                }
            } else {
                // 对于原始类型，直接添加到查询字符串数组
                queryString.push(`${fullKey}=${encodeURIComponent(value)}`);
            }
        });
    }

    // 开始序列化传入的对象
    serialize(object, parentKey);

    // 返回最终通过&符号连接的查询字符串
    return queryString.join("&");
};


/**
 * 解密JWT令牌的函数
 * @param {string} token - 要解密的JWT令牌
 * @returns {object} - 解密后的JWT令牌有效载荷数据
 */
var decryptJWT = function (token) {
    // 将所有下划线替换为斜杠，将所有破折号替换为加号，以使令牌有效的base64编码
    token = token.replace(/_/g, "/").replace(/-/g, "+");

    // 解码令牌的base64编码的有效载荷，并将其转换为JSON字符串
    const json = decodeURIComponent(escape(window.atob(token.split(".")[1])));

    // 解析JSON字符串并返回结果对象
    return JSON.parse(json);
};
/**
 * 将时间戳转换为Date对象的函数
 * @param {number} timestamp - 要转换的时间戳（单位：秒）
 * @returns {Date} - 由时间戳转换而来的Date对象
 */
var getJWTDate = function (timestamp) {
    // 通过将时间戳乘以1000来将其转换为毫秒，并创建一个新的Date对象
    return new Date(timestamp * 1000);
};


/**
 * 获取访问令牌的函数
 * @returns {string} - 包含访问令牌的字符串
 */
QB.GetToken = function () {
    const token = window.localStorage.getItem(accessTokenKey);
    if (typeof token === "undefined" || token === null) {
        const prot = window.location.protocol;
        const host = window.location.host;
        const first = window.location.pathname.split("/")[1];
        let loginUrl = prot + "//" + host + "/" + first + "/UserLoginManagement/Login.html";
        if (window.location.href.startsWith(loginUrl)) {
            return null;
        }
        loginUrl += `?returnUrl=${window.encodeURIComponent(window.location.href)}`;
        window.location.href = loginUrl;
        return null;
    } else {
        const tokenObj = JSON.parse(token);
        return tokenObj.startsWith("Bearer ") ? tokenObj : `Bearer ${tokenObj}`;
    }
};
/**
 * 清除所有访问令牌的函数
 */
QB.clearAccessTokens = () => {
    // 从 localStorage 中移除访问令牌和刷新令牌
    window.localStorage.removeItem(accessTokenKey);
    window.localStorage.removeItem(refreshAccessTokenKey);
    window.sessionStorage.removeItem(accessTokenKey);
    window.sessionStorage.removeItem(refreshAccessTokenKey);

    // 可以添加其他需要清除的键 =========================================
    // 从 sessionStorage 中移除用户信息令牌
    window.sessionStorage.removeItem(userinfoTokenKey);
    // 从 localStorage 中移除用户信息令牌
    // window.localStorage.removeItem(userinfoTokenKey);

    window.sessionStorage.removeItem(sysSessionIdKey);
};

/**
 * 获取访问令牌并根据情况添加刷新令牌到头部对象
 * @returns {Object} - 包含访问令牌和刷新令牌的头部对象
 */
QB.getHeadersWithTokenAndRefresh = function (pageObj) {
    const accessToken = this.GetToken(); // 获取访问令牌
    const headers = {
        'Authorization': `${accessToken}`,
        'x-FunCode': null,//pageObj ? pageObj.formFunData : window["F"] && F.FormFunData && F.FormFunData.FunId ? window.encodeURI(F.FormFunData.FunId) : "",
        'x-FormKey': pageObj ? pageObj.formKey : window["formKey"] ?? "",
        'x-FormParam': pageObj ? (pageObj.formDataSet.busParamName ? pageObj.formDataSet.pageParams[pageObj.formDataSet.busParamName] : "") : "",
        'x-FirstFolder': window.location.pathname.split('/')[1],
        'x-StepId': pageObj ? (pageObj.FlowStepId || "") : "",
    }; // 初始化头部对象，包含访问令牌
    const jwt = decryptJWT(accessToken); // 解析访问令牌的信息
    if (new Date() >= getJWTDate(jwt.exp)) { // 判断访问令牌是否过期
        var refreshAccessToken = JSON.parse(window.localStorage.getItem(refreshAccessTokenKey)); // 获取刷新令牌
        if (refreshAccessToken) headers["X-Authorization"] = refreshAccessToken.startsWith("Bearer ") ? refreshAccessToken : `Bearer ${refreshAccessToken}`; // 如果刷新令牌存在，添加到头部对象
    }
    return headers; // 返回头部对象
};

/**
 * 将访问令牌添加到请求中的函数
 * @param {XMLHttpRequest|Request} httprequest - 要添加令牌的请求对象
 */
QB.AddTokenToRequest = function (httprequest) {
    const headers = this.getHeadersWithTokenAndRefresh(); // 获取包含访问令牌和刷新令牌的头部对象
    if (httprequest instanceof XMLHttpRequest) {
        for (let key in headers) {
            if (Object.hasOwn(headers, key)) {
                httprequest.setRequestHeader(key, headers[key]); // 如果是XMLHttpRequest对象，使用setRequestHeader方法设置头部
            }
        }
    } else if (httprequest instanceof Request) {
        httprequest.headers = new Headers(headers); // 如果是Request对象，直接设置headers属性为包含访问令牌和刷新令牌的头部对象
    } else {
        httprequest.headers = new Headers(headers);
    }
};

/**
 * 过期后刷新token
 * @param {any} res
 */
QB.RefreshToken = function (res) {
    //var accessToken = res.getResponseHeader(accessTokenKey);
    //var refreshAccessToken = res.getResponseHeader(refreshAccessTokenKey);
    var accessToken = res.headers.get("access-token");//res.getResponseHeader("access-token");
    var refreshAccessToken = res.headers.get("x-access-token"); //res.getResponseHeader("x-access-token");
    QB.ValidateToken(accessToken, refreshAccessToken);
}

/**
 * 验证token
 * @param {any} accessToken
 * @param {any} refreshAccessToken
 */
QB.ValidateToken = function (accessToken, refreshAccessToken) {
    // 判断是否是无效 token
    if (accessToken === "invalid_token") {
        QB.LoginOut();
    }
    // 判断是否存在刷新 token，如果存在则存储在本地
    else if (
        refreshAccessToken &&
        accessToken &&
        accessToken !== "invalid_token"
    ) {
        window.localStorage.setItem(accessTokenKey, JSON.stringify(accessToken));
        window.localStorage.setItem(refreshAccessTokenKey, JSON.stringify(refreshAccessToken));
    }
}

/**
 * 防抖函数
 * @param {Function} func - 要防抖的函数
 * @param {number} delay - 在调用防抖函数之前要等待的毫秒数
 * @returns {Function} - 返回一个函数
 */
QB.debounce = function (func, delay) {
    let timerId; // 用于存储定时器ID的变量

    /**
     * 包装函数，返回一个Promise对象
     * @returns {Promise} - 返回一个Promise对象
     */
    return function () {
        return new Promise((resolve, reject) => { // 返回一个Promise对象
            clearTimeout(timerId); // 清除之前的定时器

            timerId = setTimeout(() => { // 设置新的定时器
                try {
                    const result = func.apply(this, arguments); // 调用原始函数
                    timerId = null; // 清空定时器ID
                    resolve(result); // 解析Promise并返回函数调用的结果
                } catch (error) {
                    reject(error); // 如果函数调用出错，则拒绝Promise并返回错误
                }
            },
                delay);
        });
    };
};

/**
 * 节流函数
 * @param {Function} func - 要节流的函数
 * @param {number} delay - 两次执行之间的时间间隔
 * @returns {Function} - 返回一个函数
 */
QB.throttle = function (func, delay) {
    let isExecuting = false; // 用于标记函数是否正在执行
    let savedArgs; // 用于存储函数参数的变量

    return function (...args) { // 返回一个新的函数
        savedArgs = args; // 保存函数参数
        return new Promise((resolve, reject) => { // 返回一个Promise对象
            if (!isExecuting) { // 如果函数没有在执行
                isExecuting = true; // 标记函数正在执行
                setTimeout(() => { // 设置定时器
                    try {
                        func.apply(this, savedArgs); // 调用原始函数
                        isExecuting = false; // 标记函数执行完成
                        resolve(); // 解析Promise
                    } catch (error) {
                        reject(error); // 如果函数调用出错，则拒绝Promise并返回错误
                    }
                },
                    delay);
            }
        });
    };
};


QB.LoginOut = function () {

    QB.CreateRequest(webApiRoot + "LoginAuthority/UserLogoutByEnt", "GET", null, true).then((result) => {
        const jsonData = result.data;
        if (jsonData.Type != "success") {
            console.log("退出失败!");
        }
        let prot = window.location.protocol;
        let host = window.location.host;
        let userInfo = window.sessionStorage.getItem(userinfoTokenKey);
        let first = userInfo ? JSON.parse(userInfo).userInfo.EntShortName : window.location.pathname.split('/')[1];
        QB.clearAccessTokens();
        window.location.href = prot + "//" + host + "/" + first + "/UserLoginManagement/Login.html?returnUrl=" + window.encodeURIComponent(window.location.href);
    });
}



QB.delayManager = {
    timers: {}, // 存储所有定时器的对象

    /**
     * 在特定条件满足时设置一个计时器，返回一个 Promise。
     * @param {string} id - 计时器的唯一标识。
     * @param {Function} condition - 返回布尔值的函数，用于检查是否满足条件。
     * @param {number} interval - 检查条件的时间间隔（毫秒）。
     * @param {number} maxAttempts - 尝试满足条件的最大次数。
     * @returns {Promise} 在条件满足时解决的 Promise。
     */
    setConditionalTimer: function (id, condition, interval, maxAttempts) {
        return new Promise((resolve, reject) => {
            if (typeof condition !== 'function') {
                reject(new Error('Condition 必须是返回布尔值的函数'));
            }
            this.cancel(id); // 先取消同 ID 的延迟执行
            let attempts = 0;
            const checkCondition = () => {
                //console.log(attempts);
                if (condition()) {
                    this.cancel(id); // 条件满足，先取消定时器
                    resolve(); // 解决 Promise
                } else if (attempts < maxAttempts) {
                    attempts++;
                    this.timers[id] = setTimeout(checkCondition, interval); // 重新设置定时器以后续检查条件
                } else {
                    console.log('达到最大尝试次数，取消定时器');
                    this.cancel(id); // 达到最大尝试次数，取消定时器
                    reject(new Error('达到最大尝试次数'));
                }
            };
            this.timers[id] = setTimeout(checkCondition, interval); // 设置初始定时器检查条件
        });
    },

    /**
     * 取消具有特定 ID 的延迟执行。
     * @param {string} id - 需要取消的延迟执行的唯一标识。
     */
    cancel: function (id) {
        if (this.timers[id]) {
            clearTimeout(this.timers[id]); // 清除定时器
            delete this.timers[id]; // 删除记录
        }
    },

    /**
     * 取消所有当前活动的延迟执行。
     */
    cancelAll: function () {
        Object.keys(this.timers).forEach(id => {
            clearTimeout(this.timers[id]); // 清除每个定时器
        });
        this.timers = {}; // 重置定时器记录对象
    }
};



/**
 * 使用localStorage进行数据缓存的管理器。
 */
QB.CacheManager = class {
    /**
     * 创建 CacheManager 的实例。
     * @param {string} category - 要缓存的数据类别。
     * @param {string} [baseUrl='https://api.example.com/data'] - 获取数据的API的基础URL。
     */
    constructor(category, baseUrl = 'https://api.example.com/data') {
        this.category = category; // 数据类别，用于区分不同类型的缓存数据
        this.baseUrl = baseUrl; // API基础URL，用于获取数据
        this.memoryCache = {}; // 内存缓存对象,备用方案
        //this.clearCategoryCache(); //清除同类缓存
    }

    /**
     * 为给定的键生成一个缓存键。
     * @param {string} key - 要为其生成缓存键的键。
     * @returns {string} 生成的缓存键。
     */
    generateCacheKey(key) {
        // 使用数据类别和键来生成唯一的缓存键
        return `${this.category}:${key}`;
    }

    /**
     * 为给定的键缓存数据，并记录一个时间戳。
     * @param {string} key - 要存储数据的缓存键。
     * @param {object} data - 要缓存的数据。
     * @param {number} ttl - 缓存时间(以分钟为单位)。
     */
    setCache(key, data, ttl = 24 * 60) {
        try {
            const item = {
                data: data,
                expiry: Date.now() + ttl * 60000
            };
            localStorage.setItem(key, JSON.stringify(item));
        } catch (error) {
            // 捕获并处理异常
            console.error('设置缓存数据时出错：', error);
            //console.warn('localStorage is not available, using memory cache.');
            // 使用内存缓存，并设置过期时间
            this.memoryCache[key] = {
                data: data,
                expiry: Date.now() + ttl * 60000
            };
        }
    }

    /**
     * 获取给定键的缓存数据。
     * @param {string} key - 要检索数据的缓存键。
     * @returns {object|null} 缓存的数据，如果没有找到或已过期则为null。
     */
    getCache(key) {
        try {
            const itemStr = localStorage.getItem(key);
            if (!itemStr) {
                return null;
            }
            const item = JSON.parse(itemStr);
            const now = new Date();

            if (now.getTime() > item.expiry) {
                localStorage.removeItem(key);
                return null;
            }
            return item.data;
        } catch (error) {
            console.warn('localStorage is not available, using memory cache.');
            return this.memoryCache[key] || null;
        }
    }

    /**
     * 清除与此类别相关的所有缓存数据。
     */
    clearCategoryCache() {
        try {
            const keys = Object.keys(localStorage);
            for (const key of keys) {
                if (key.startsWith(this.category + ':')) {
                    localStorage.removeItem(key);
                }
            }
        } catch (error) {
            console.warn('localStorage is not available, using memory cache.');
            const keys = Object.keys(this.memoryCache);
            for (const key of keys) {
                if (key.startsWith(this.category + ':')) {
                    delete this.memoryCache[key];
                }
            }
        }
    }

    /**
     * 清除所有已过期的缓存项。
     */

    static clearExpiredCache() {
        const now = Date.now();
        for (const [key, itemStr] of Object.entries(localStorage)) {
            try {
                if (itemStr) {
                    const item = JSON.parse(itemStr);
                    if (now > item.expiry) {
                        localStorage.removeItem(key);
                    }
                }
            } catch (error) {
                console.error('检查过期缓存时出错：', error);
                localStorage.removeItem(key);
            }
        }
    }

    /**
     * 添加一个方法来构建URL。
     * @param {string} key - 要获取数据的键。
     * @returns {URL} 构建的URL对象。
     */
    createFetchUrl(key) {
        // 创建一个新的URL对象
        const url = new URL(this.baseUrl);
        // 向URL添加查询参数
        url.searchParams.append('key', key);
        url.searchParams.append('category', this.category);
        return url;
    }

    /**
     * 获取给定键的数据，尽可能使用缓存。
     * @param {string} key - 要获取数据的键。
     * @returns {Promise<object>} 解析为获取的数据的promise。
     */
    async getData(key) {
        const cacheKey = this.generateCacheKey(key);
        // 尝试从缓存中获取数据
        const cachedData = this.getCache(cacheKey);
        // 构建API请求的URL
        const url = this.createFetchUrl(key);

        // 如果缓存中有数据，并且含有时间戳，则添加时间戳参数
        if (cachedData && cachedData.timestamp) {
            url.searchParams.append('timestamp', cachedData.timestamp);
        }

        try {
            // 发送网络请求
            const response = await fetch(url.toString());
            if (!response.ok) {
                // 如果响应状态不是OK，则抛出错误
                throw new Error(`HTTP错误！状态：${response.status}，请求URL：${url}`);
            }
            // 解析响应数据
            const responseData = await response.json();

            // 检查时间戳，如果数据是新的，则更新缓存
            if (responseData.timestamp && (!cachedData || cachedData.timestamp !== responseData.timestamp)) {
                this.setCache(cacheKey, responseData);
                return responseData.data;
            } else {
                // 如果数据没有更新，返回缓存数据
                return cachedData ? cachedData.data : null;
            }
        } catch (error) {
            // 捕获并记录任何错误
            console.error('获取数据时出错：', error.message);
            throw error; // 将错误向上抛出
        }
    }

    /**
     * 获取给定键的数据，尽可能使用缓存。
     * @param {string} key - 要获取数据的键。
     * @returns {Promise<object>} 解析为获取的数据的promise。
     */
    async VueGetData(key) {
        const cacheKey = this.generateCacheKey(key);
        // 尝试从缓存中获取数据
        const cachedData = this.getCache(cacheKey);
        // 构建API请求的URL
        const url = this.createFetchUrl(key).toString();

        // 如果缓存中有数据，并且含有时间戳，则添加时间戳参数
        if (cachedData && cachedData.timestamp) {
            url.searchParams.append('timestamp', cachedData.timestamp);
        }

        try {
            // 使用 axios 发送网络请求
            const response = await axios.get(url);
            // 解析响应数据
            const responseData = response.data;

            // 检查时间戳，如果数据是新的，则更新缓存
            if (responseData.timestamp && (!cachedData || cachedData.timestamp !== responseData.timestamp)) {
                this.setCache(cacheKey, responseData);
                return responseData.data;
            } else {
                // 如果数据没有更新，返回缓存数据
                return cachedData ? cachedData.data : null;
            }
        } catch (error) {
            // 捕获并记录任何错误
            console.error('获取数据时出错：', error.message);
            throw error; // 将错误向上抛出
        }
    }
}



/**
 * 自定义提示框
 * @param {any} message
 * @param {any} buttons
 * @param {any} width
 * @param {any} height
 * @param {any} autoCloseSeconds
 */
QB.customAlert = function (message, buttons, width, height, autoCloseSeconds) {
    // 创建遮罩层
    var mask = document.createElement('div');
    mask.className = 'custom-alert-mask';
    document.body.appendChild(mask);

    // 创建一个弹出框
    var alertBox = document.createElement('div');
    alertBox.className = 'custom-alert';
    alertBox.style.width = width + 'px';
    alertBox.style.height = height + 'px';

    // 添加提示文字
    var messageElement = document.createElement('p');
    messageElement.textContent = message;
    alertBox.appendChild(messageElement);

    // 创建按钮容器
    var buttonContainer = document.createElement('div');
    buttonContainer.style.textAlign = 'right'; // 按钮居右
    alertBox.appendChild(buttonContainer);

    // 添加按钮
    if (buttons && buttons.length > 0) {
        buttons.forEach(function (button, index) {
            var buttonElement = document.createElement('button');
            buttonElement.className = "layui-btn";
            buttonElement.textContent = button.label;
            if (index < buttons.length - 1) {
                buttonElement.style.marginRight = '10px'; // 添加按钮间隔
            }
            //var btnObj = new ejs.buttons.Button({
            //    content: button.label,
            //});
            //btnObj.appendTo(buttonElement);
            buttonElement.addEventListener('click', function () {
                // 点击按钮时触发回调函数
                if (typeof button.callback === 'function') {
                    button.callback();
                }
                // 移除弹出框和遮罩层
                alertBox.parentNode.removeChild(alertBox);
                mask.parentNode.removeChild(mask);
            });
            buttonContainer.appendChild(buttonElement);
        });
    } else {
        //如果按钮没有设置，并且自动关闭参数为0，将自动关闭参数设置为3
        if (autoCloseSeconds == 0) {
            autoCloseSeconds = 3;
        }
    }

    // 将弹出框添加到页面中
    document.body.appendChild(alertBox);

    // 设置样式
    var style = document.createElement('style');
    style.textContent = '.custom-alert { position: fixed; top: 10%; left: 50%; transform: translate(-50%, -50%); background: #fff; padding: 20px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); z-index: 9999; } .custom-alert-mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 9998; }';
    document.head.appendChild(style);

    // 自动关闭弹出框
    if (autoCloseSeconds > 0) {
        setTimeout(function () {
            alertBox.parentNode.removeChild(alertBox);
            mask.parentNode.removeChild(mask);
        }, autoCloseSeconds * 1000);
    }
}


/**
 * 替换元素的class，没有直接添加
 * @param {Element} ele 元素
 * @param {string} className 类名
 * @param {string} newClassName 新类名
 */
QB.ReplaceClass = function (ele, className, newClassName) {
    if (ele.classList.contains(className)) {
        ele.classList.remove(className);
    }
    if (newClassName)
        ele.classList.add(newClassName);
}


QB.Connection = function () {

    // // 初始化SignalR对象
    // var connection = new signalR.HubConnectionBuilder()
    //     //.withUrl(`http://fishwy.nat300.top/hubs/onlineUser?access_token=${token.replace("Bearer ", "")}`).   //SignalR服务器地址
    //     //.withUrl(`https://localhost:44339/hubs/onlineUser?access_token=${QB.GetToken().replace("Bearer ", "")}`).
    //     //.withUrl(`/hubs/onlineUser?access_token=${QB.GetToken().replace("Bearer ", "")}`).
    //     .withUrl("/api/websocket", {
    //         accessTokenFactory: () => { return QB.GetToken().replace('Bearer ', ''); },
    //         skipNegotiation: true,  // 重要：跳过协商过程
    //         transport: signalR.HttpTransportType.WebSockets  // 强制使用 WebSocket
    //     })
    //     .withAutomaticReconnect({                                                                            //当连接自动断开,自动尝试重新建立连接
    //         nextRetryDelayInMilliseconds: () => {                                                           //设置自动连接为5秒
    //             return 5000; // 每5秒重连一次
    //         },
    //     })
    //     .build();
    // connection.keepAliveIntervalInMilliseconds = 15 * 1000; // 心跳检测15s                                  //当客户端与服务端在15秒时间内未发生交互时，服务端将自动断开，并尝试创建连接
    // connection.serverTimeoutInMilliseconds = 30 * 1000; // 超时时间30m                                 //当服务端在30秒时间内未收到客户端的消息，服务端认为连接断开，并尝试创建连接
    // // 启动连接
    // connection.start().then(() => {
    //     //debugger;
    //     console.log('启动连接');
    //     // Send a handshake request manually if needed
    //     const handshakeRequest = JSON.stringify({
    //         protocol: 'json',
    //         version: 1
    //     });
    //     connection.invoke("SendMessage", handshakeRequest);
    // });
    // // 断开连接
    // connection.onclose(async () => {
    //     //debugger;
    //     console.log('断开连接');
    // });
    // // 重连中
    // connection.onreconnecting(() => {
    //     ElNotification({
    //         title: '提示',
    //         message: '服务器已断线...',
    //         type: 'error',
    //         position: 'bottom-right',
    //     });
    // });
    // // 重连成功
    // connection.onreconnected(() => {
    //     console.log('重连成功');
    // });
    //
    // //接收服务端短信
    // //connection.off('ReceiveMessage');
    // //connection.on('ReceiveMessage', (data) => {
    // //    debugger;
    //
    // //    var jsonData = JSON.parse(data.message);
    // //    if (jsonData.Type == "notify") {
    // //        if (SysNavigation) {
    // //            SysNavigation.notifyNumSpan.innerText = jsonData.Num;
    // //        }
    // //    }
    // //});
    //
    // return connection;
}

QB.ConnectionWebSocket = function () {
    let ws; // WebSocket 实例
    let heartbeatInterval; // 心跳定时器
    let reconnectTimeout; // 重连定时器
    const HEARTBEAT_INTERVAL = 5000; // 心跳间隔（30秒）
    const RECONNECT_DELAY = 5000; // 重连延迟（5秒）

    function createWebSocket() {
        let token = QB.GetToken();
        const prifiex = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
        let host = window.location.host;
        if (token) {
            token = token.replace("Bearer ", "")
            ws = new WebSocket(`${prifiex}${host}${window.location.port ? ':' + window.location.port : ''}/api/websocket`, ["protocol1", `${token}`]);
        } else {
            ws = new WebSocket(`${prifiex}${host}${window.location.port ? ':' + window.location.port : ''}/api/websocket`, ["protocol1"]);
        }
        // ws = new WebSocket(`wss://${window.location.host}${window.location.port ? ':' + window.location.port : ''}/api/websocket`, ["protocol1",`${token}`]); // 创建 WebSocket 实例，使用当前主机和端口

        ws.onopen = () => {
            console.log('WebSocket connected');
            const data = JSON.stringify({
                messageType: 'SYSTEM_NOTIFICATION',
                title: 'GET_SYS_SESSION_ID',
                content: 'GET_SYS_SESSION_ID'
            })
            //没有token就发送
            if (!token) {
                ws.send(data)
            }
            startHeartbeat(); // 开始心跳监测
        };

        ws.onmessage = (event) => {
            handleServerMessage(event.data); // 处理服务器消息
        };

        ws.onerror = (error) => {
            console.error('WebSocket error:', error);
            reconnect(); // 发生错误时尝试重连
        };

        ws.onclose = (event) => {
            console.log('WebSocket closed:', event.code, event.reason);
            stopHeartbeat(); // 停止心跳监测
            reconnect(); // 连接关闭时尝试重连
        };
    }

    function startHeartbeat() {
        stopHeartbeat(); // 确保不会有重复的定时器

        heartbeatInterval = setInterval(() => {
            if (ws.readyState === WebSocket.OPEN) {
                //console.log('Sending heartbeat');
                ws.send('ping'); // 发送心跳消息
            } else {
                console.warn('WebSocket is not open, stopping heartbeat');
                stopHeartbeat();
            }
        }, HEARTBEAT_INTERVAL);
    }

    function stopHeartbeat() {
        if (heartbeatInterval) {
            clearInterval(heartbeatInterval);
            heartbeatInterval = null;
        }
    }


    function handleServerMessage(message) {
        if (message === 'pong') {
            //console.log('Heartbeat response received');
        } else {
            //后续接收后端消息，关于后台数据变动，通知前端正在使用的页面涉及的数据，提示需要刷新，页面根据需求判断强制刷新或由操作人选择刷新
            console.log('Other message:', message);
            let msgObj = JSON.parse(message);
            if (msgObj.messageType === 'TIP') {
                /**
                 * 提示消息：不需要记录数据库
                 */
                layui.layer.msg(msgObj.content);
            } else if (msgObj.messageType === 'USER_MESSAGE') {
                /**
                 * 用户消息：用户之间的消息发送
                 */

            } else if (msgObj.messageType === 'ACKNOWLEDGMENT') {
                /**
                 * 确认消息：确认消息接收到
                 */

            } else if (msgObj.messageType === 'SYSTEM_NOTIFICATION') {
                /**
                 * 系统消息：系统消息
                 */
                console.log('System message:', msgObj);
                switch (msgObj.title) {

                    case 'SYS_SESSION_ID':
                        //将将链接id存储在sessionStorage中
                        window.localStorage.setItem(sysSessionIdKey, msgObj.content);
                        window.sessionStorage.setItem(sysSessionIdKey, msgObj.content);
                        break;
                    case 'SYS_LOGIN_SUCCESS':
                        const token = JSON.parse(msgObj.content);
                        function toRedicct() {
                            let returnUrl = getReturnUrl()
                            if (returnUrl) {
                                window.location.href = window.encodeURI(
                                    window.decodeURIComponent(returnUrl)
                                )
                            } else {
                                window.location.href = window.encodeURI(
                                    QB.GetEntFullPath(
                                        'PortalWeb/PersonalSystem.html'
                                    )
                                )
                            }
                        }

                        function getReturnUrl() {
                            const queryString = window.location.search
                            const urlParams = new URLSearchParams(
                                queryString
                            )
                            return urlParams.get('returnUrl')
                        }
                        toRedicct()//跳转到首页
                        //本地存储令牌
                        window.localStorage.setItem(accessTokenKey, JSON.stringify(token.token.replace("Bearer ", "")));
                        window.localStorage.setItem(refreshAccessTokenKey, JSON.stringify(token.refreshToken.replace("Bearer ", "")));
                        window.sessionStorage.setItem(accessTokenKey, JSON.stringify(token.token.replace("Bearer ", "")));
                        window.sessionStorage.setItem(refreshAccessTokenKey, JSON.stringify(token.refreshToken.replace("Bearer ", "")));
                        //本地登录用户信息//移除掉

                        window.sessionStorage.removeItem(userinfoTokenKey);
                        break;
                    case "SYS_UNBOUND":
                        //未绑定跳转到绑定页面
                        const content = JSON.parse(msgObj.content);
                        console.log(content);
                        const params = new URLSearchParams(content).toString();
                        //没有绑定跳转到绑定页面
                        window.location.href = `${window.location.protocol}//${window.location.host}/NewApp/UserLoginManagement/AccountBinding.html?"${params}`
                        break;
                }
                /**
                 * 系统通知消息：系统发送的通知
                 */
                if (msgObj.title === '退出登录') {
                    QB.clearAccessTokens();
                    ws.close(1000, '正常关闭');
                    let prot = window.location.protocol;
                    let host = window.location.host;
                    let first = window.location.pathname.split('/')[1];
                    window.location.href = prot + "//" + host + "/" + first + "/UserLoginManagement/Login.html?returnUrl=" + window.encodeURIComponent(window.location.href);
                }
            } else if (msgObj.messageType === 'SYSTEM_DOWNLOAD') {
                /**
                 * 系统下载消息：系统发送的下载链接或资源信息
                 */
                let fileObj = JSON.parse(msgObj.content);
                QB.FileDownload(fileObj.fileName, fileObj.filePath, fileObj.appType);
            } else if (msgObj.messageType === 'SYSTEM_ERROR') {
                /**
                 * 系统错误消息
                 */

            }
        }
    }

    function reconnect() {
        if (reconnectTimeout) {
            return; // 防止重复重连
        }

        console.log(`Attempting to reconnect in ${RECONNECT_DELAY / 1000} seconds...`);
        reconnectTimeout = setTimeout(() => {
            console.log('Reconnecting...');
            createWebSocket(); // 重新创建 WebSocket 连接
            reconnectTimeout = null; // 清除重连定时器
        }, RECONNECT_DELAY);
    }

    // 启动 WebSocket 连接
    createWebSocket();


    window["QyWebSocket"] = ws;
    return ws;
}

QB.ConvertValueById = function (elementId) {
    let element = document.getElementById(elementId);
    let inputValue = element.value;
    let targetType = element.type;

    switch (targetType) {
        case 'text':
        case 'email':
        case 'password':
        case 'search':
        case 'tel':
        case 'url':
            return String(inputValue);
        case 'number':
        case 'range':
            return Number(inputValue);
        case 'checkbox':
        case 'radio':
            return element.checked;
        case 'date':
        case 'month':
        case 'week':
        case 'time':
        case 'datetime-local':
            return new Date(inputValue);
        case 'color':
            return inputValue; // 返回颜色值
        case 'file':
            return element.files[0]; // 返回文件对象
        default:
            return inputValue;
    }
}

/**
 * 导入自定义组件js模块
 * @param element
 * @param pageUrl
 * @param pageParams
 * @param checkLogin
 * @returns {Promise<void>}
 */
QB.loadModule = async function (element, pageUrl, pageParams, checkLogin = true, stepId = "") {
    return new Promise(async (resolve, reject) => {
        try {
            // 更新 URL 的参数  
            let fileUrl = pageUrl.replace(window.location.origin, "");
            let fileParam = new FormData();
            fileParam.append("fileUrl", fileUrl);

            let hasFile = true;
            await QB.CreateRequest(webApiRoot + "File/lastModifiedTime", "POST", fileParam, checkLogin)
                .then((result) => {
                    if (result.data.Type == "success") {
                        pageUrl += ("?v=" + result.data.Result);
                    } else if (result.data.Message && result.data.Message.includes("404")) {
                        hasFile = false;
                    }
                });

            if (!hasFile) {
                reject("404");
                return;
            }

            // 动态导入模块  
            const module = await import(pageUrl);

            // 调用导入模块的逻辑  
            let newpage = new module.QYPage();
            newpage.pageParams = pageParams;
            newpage.FlowStepId = stepId;
            newpage.render(element);

            // 如果所有代码执行完毕，resolve  
            resolve(newpage);
        } catch (error) {
            // 如果出现错误，reject，并记录错误  
            console.error('Error loading module:', error);
            reject(error);
        }
    });
};

/**
 * 解析字符串，返回scripts和links，meta
 */
QB.parseAndAddTags = function (htmlString) {
    return new Promise((resolve, reject) => {
        // 使用 DOMParser 解析 HTML 字符串
        const parser = new DOMParser();
        const doc = parser.parseFromString(htmlString, 'text/html');

        // 提取 <script> 标签的 src 属性
        const scriptTags = doc.querySelectorAll('script[src]');
        const scriptSources = Array.from(scriptTags).map(script => script.src);

        // 提取 <link> 标签的 href 属性
        const linkTags = doc.querySelectorAll('link[href]');
        const linkHrefs = Array.from(linkTags).map(link => link.href);

        // 提取 <meta> 标签的 name 和 content 属性
        const metaTags = doc.querySelectorAll('meta[name]');
        const metaInfo = Array.from(metaTags).map(meta => ({
            name: meta.name,
            content: meta.content
        }));

        // 用于跟踪加载的 Promise 数组
        const loadPromises = [];

        // 动态添加 <meta> 标签到页面
        metaInfo.forEach(({ name, content }) => {
            const meta = document.createElement('meta');
            meta.name = name;
            meta.content = content;
            document.head.appendChild(meta);
        });

        // 动态添加 <link> 标签到页面并跟踪加载
        linkHrefs.forEach(href => {
            const link = document.createElement('link');
            link.rel = 'stylesheet';
            link.href = href;
            document.head.appendChild(link);

            // 创建一个 Promise 来跟踪样式加载
            const linkPromise = new Promise((resolve, reject) => {
                link.onload = resolve;
                link.onerror = reject;
            });
            loadPromises.push(linkPromise);
        });

        // 动态添加 <script> 标签到页面并跟踪加载
        scriptSources.forEach(src => {
            const existingScript = document.querySelector(`script[src="${src}"]:not(.QYDisplay.hideCssAndJS script)`);
            if (!existingScript) {
                const script = document.createElement('script');
                script.src = src;
                document.body.appendChild(script);

                // 创建一个 Promise 来跟踪脚本加载
                const scriptPromise = new Promise((resolve, reject) => {
                    script.onload = resolve;
                    script.onerror = reject;
                });
                loadPromises.push(scriptPromise);
            }
        });

        // 等待所有加载完成
        Promise.all(loadPromises)
            .then(() => resolve({
                scripts: scriptSources,
                links: linkHrefs,
                metas: metaInfo
            }))
            .catch(reject);
    });
}


/**
 * 获取父级grid行下标
 * @param {any} mayElemet
 * @returns
 */
QB.GetParentRowIndex = (mayElemet) => {
    return mayElemet.closest(".e-row").rowIndex;
}

/**
 * 创建观察者，判断指定元素是否被添加到documentElement
 * @param {document} targetNode 被观察者
 * @param {document|string} appendElement 添加的元素或元素id
 * @param {function} callback 回调方法
 */
QB.CreateMutaionObserver = (targetNode, appendElement, callback) => {
    // 配置观察选项
    const config = {
        childList: true, // 观察目标节点的子节点的变动
        subtree: true // 观察所有子节点
    };

    // 创建一个观察者实例并传入回调函数
    const observer = new MutationObserver((mutationsList) => {
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                // 使用 querySelector 查找文档中具有指定 id 的元素
                if (typeof appendElement == "string") {
                    const foundNode = targetNode.querySelector(`#${appendElement}`);
                    if (foundNode) {
                        // 停止观察
                        observer.disconnect();
                        // 清除超时
                        clearTimeout(timeoutId);

                        callback(foundNode);
                    }
                } else if (document.contains(appendElement)) {
                    // 停止观察
                    observer.disconnect();
                    // 清除超时
                    clearTimeout(timeoutId);

                    callback(appendElement);
                    callback(appendElement);
                }
            }
        }
    });

    // 开始观察目标节点
    observer.observe(targetNode, config);

    // 设置一个超时，在指定时间后执行
    const timeoutId = setTimeout(() => {
        console.log('Target element was not added within the time limit.');
        // 停止观察
        observer.disconnect();
        console.log('Observer disconnected due to timeout');
        // 在这里可以添加更多的逻辑来处理超时情况
    }, 10000); // 10秒后超时
}




/**
 * 本地获取用户信息
 * @returns {object}
 */
QB.GetUserInfo = function () {
    let userInfo = window.sessionStorage.getItem(userinfoTokenKey);
    if (!userInfo) {
        QB.LoginOut();
    } else {
        return JSON.parse(userInfo).userInfo;
    }
}

/**
 * 根据登录用户获取企业完整路径
 * @param {string} url 相对地址
 * @returns {string}
 */
QB.GetEntFullPath = function (url) {
    if (!url.toLowerCase().startsWith("http")) {
        if (url.startsWith("SysForm") || url.startsWith("/SysForm")) {
            return window.encodeURI(`${window.location.origin}${(url.startsWith("/") ? url : ("/" + url))}`);
        } else {
            const userObj = QB.GetUserInfo();
            return window.encodeURI(`${window.location.origin}/${userObj.EntShortName}${(url.startsWith("/") ? url : ("/" + url))}`);
        }
    } else {
        return url;
    }
}

/**
 * 获取页面js路径及参数集合
 * @param url
 * @returns {{params: *[], url: string}}
 * @constructor
 */
QB.GetPageJsUrlAndParams = function (url, loadPageJs = false) {
    let returnObj = {
        url: "",
        params: []
    };
    if (!url) return returnObj;
    loadPageJs = loadPageJs || !url.toLowerCase().startsWith('http');
    returnObj.url = url;
    if (!loadPageJs) {
        return returnObj;
    }

    returnObj.url = QB.GetEntFullPath(url);
    returnObj.url = returnObj.url.replace(".aspx", ".page.js").replace(".html", ".page.js");

    // 创建一个 URL 对象
    const urlObj = new URL(returnObj.url);
    // 获取查询参数
    const params = new URLSearchParams(urlObj.search);
    // 将参数转换为 [{key: "", value: ""}] 格式的数组
    const result = [];
    // 解析查询参数
    for (const [key, value] of params.entries()) {
        returnObj.params.push({ key, value });
    }
    // 如果有哈希部分，添加到参数列表
    if (urlObj.hash) {
        returnObj.params.push({ key: "hash", value: urlObj.hash.substring(1) }); // 去掉开头的 #
    }
    returnObj.url = urlObj.origin + urlObj.pathname;

    return returnObj;
}


QB.GetEventFormObj = function (event) {
    let layItemEle = event.srcElement.closest(".qyformdiv");
    let sourceForm = layItemEle ? layItemEle.querySelector("#Form1") : document.activeElement.closest("#Form1");
    if (sourceForm) {
        let eventFormObj = {
            formKey: "",
            modid: "",
        };
        eventFormObj.formKey = sourceForm.className.replace("Form_", "").replace("e-sidebar-context", "").trim();
        eventFormObj.modid = sourceForm.dataset.modid;
        return eventFormObj;
    } else {
        return null;
    }
}

/**打开表单编辑页面*/
QB.OpenFormEdit = function (event) {

    let eventFormObj = QB.GetEventFormObj(event);
    if (!eventFormObj) {
        console.log("请先点击需要设计的窗体内部元素，然后重复此操作！");
        return;
    }

    //let suburl = QB.GetEntFullPath("/DataCenter/AppManagement/content/form/Designer/Main.html?formkey=" + eventFormObj.formKey + "&modid=" + eventFormObj.modid);
    let suburl = QB.GetEntFullPath("/SysForm/FormDesign/FormDesignPage/Main.html?formkey=" + eventFormObj.formKey);
    if (event.shiftKey) {
        suburl = QB.GetEntFullPath("SysForm/VisualEditing.html?formKey=" + eventFormObj.formKey);
    }
    window.open(suburl);
}

/**打开bug管理页面*/
QB.OpenBugManage = function () {
    let eventFormObj = QB.GetEventFormObj(event);
    if (!eventFormObj) {
        console.log("请先点击需要提交BUG的窗体内部元素，然后重复此操作！");
        return;
    }
    const bugPageUrl = QB.GetEntFullPath(`/PlatProjectManage/Bug_Form_xq.html?flownodeid=&rowid=&sysid=&modid=${eventFormObj.modid}&formkey=${eventFormObj.formKey}`);
    window.open(bugPageUrl);
}

/**
 * 文件上传
 * @param {File} files
 * @param {string} path
 * @param {string} appType
 * @param {boolean} isReplace
 * @param {string} newName
 * @returns
 */
QB.FileUpload = function (files, filePath, appType, isReplace, newName) {
    return new Promise((resolve, reject) => {
        const formData = new FormData();
        formData.append("files", files);
        formData.append("customPath", filePath);
        formData.append("appType", appType);
        formData.append("isReplace", isReplace ? true : false);
        formData.append("newName", newName || "");

        QB.CreateRequest(`${webApiRoot}File/UploadFile`, "POST", formData, true).then((result) => {
            if (result.data.Type === "success") {
                resolve(result.data);
            } else {
                if (result.data.Message === "401 登录已过期，请重新登录") {
                    LoginOut();
                } else {
                    reject(result.data);
                }
            }
        }).catch((err) => {
            reject(err)
        });
    })
}

/**
 * 文件移除
 * @param {fileName} fileName
 * @param {string} path
 * @param {string} appType
 * @returns
 */
QB.FileDelete = function (fileName, filePath, appType) {
    return new Promise((resolve, reject) => {
        const formData = new FormData();
        formData.append("fileName", fileName);
        formData.append("customPath", filePath);
        formData.append("appType", appType);
        //let formData = {
        //    fileName,
        //    customPath: filePath,
        //    appType
        //};

        QB.CreateRequest(`${webApiRoot}File/RemoveFile`, "POST", formData, true).then((result) => {
            if (result.data.Type === "success") {
                resolve(result.data);
            } else {
                if (result.data.Message === "401 登录已过期，请重新登录") {
                    LoginOut();
                } else {
                    reject(result.data);
                }
            }
        }).catch((err) => {
            reject(err)
        });
    });
}

/**
 * 文件下载
 * @param {string} name 文件名称
 * @param {string} filePath 文件路径
 * @param {string} appType 应用类型
 */
QB.FileDownload = function (fileName, filePath, appType, isCrossEnt = true) {
    var param = {
        fileName: fileName,
        customPath: filePath,
        appType: appType,
        isCrossEnt: isCrossEnt
    };

    QB.CreateRequest(`${webApiRoot}File/DownFile`, "POST", param, true, "blob").then((result) => {
        const url = window.URL.createObjectURL(result.data);
        const a = document.createElement('a');
        a.style.display = 'none';
        a.href = url;
        a.download = fileName; // 下载时显示的文件名
        document.body.appendChild(a);
        a.click();
        window.URL.revokeObjectURL(url);
    }).catch((err) => {
        console.error(err);
    });
}

/**
 * 创建文件夹
 * @param {string} folderName 文件夹名称
 * @param {string} path 路径
 * @param {string} appType 应用类型
 * @returns
 */
QB.CreateFolder = function (folderName, path, appType) {
    return new Promise((resolve, reject) => {
        var param = {
            Action: "create",
            Name: folderName,
            Path: path,
            appType: appType,
        };

        QB.CreateRequest(`${webApiRoot}File/FileManagerFileOperations`, "POST", param, true).then((result) => {
            if (typeof result.data == "string" || result.data.Type === "success") {
                resolve(result.data);
            } else {
                if (result.data.Message === "401 登录已过期，请重新登录") {
                    LoginOut();

                } else {
                    reject(result.data);
                }
            }
        }).catch((err) => {
            reject(err)
        });
    })
}

/**
 * 文件赋值
 * @param {string} name 文件名称
 * @param {string} path 路径
 * @param {string} targetPath 目标路径
 * @param {string} appType 应用类型
 * @returns
 */
QB.FileCopy = function (name, path, targetPath, appType) {
    return new Promise((resolve, reject) => {
        var param = {
            Action: "copy",
            Names: [name],
            RenameFiles: [name],
            Path: path,
            TargetPath: targetPath,
            appType: appType,
        };

        QB.CreateRequest(`${webApiRoot}File/FileManagerFileOperations`, "POST", param, true).then((result) => {
            if (typeof result.data == "string" || result.data.Type === "success") {
                resolve(result.data);
            } else {
                if (result.data.Message === "401 登录已过期，请重新登录") {
                    LoginOut();

                } else {
                    reject(result.data);
                }
            }
        }).catch((err) => {
            reject(err)
        });
    })
}

/**
 * 文件移动
 * @param {string} name 文件名称
 * @param {string} path 路径
 * @param {string} targetPath 目标路径
 * @param {string} appType 应用类型
 * @returns
 */
QB.FileMove = function (name, path, targetPath, appType) {
    return new Promise((resolve, reject) => {
        var param = {
            Action: "move",
            Names: [name],
            RenameFiles: [name],
            Path: path,
            TargetPath: targetPath,
            appType: appType,
        };

        QB.CreateRequest(`${webApiRoot}File/FileManagerFileOperations`, "POST", param, true).then((result) => {
            if (typeof result.data == "string" || result.data.Type === "success") {
                resolve(result.data);
            } else {
                if (result.data.Message === "401 登录已过期，请重新登录") {
                    LoginOut();

                } else {
                    reject(result.data);
                }
            }
        }).catch((err) => {
            reject(err)
        });
    })
}

/**
 * 文件重命名
 * @param {string} name 文件名称
 * @param {string} newName 新名称
 * @param {string} path 路径
 * @param {string} appType 应用类型
 * @returns
 */
QB.FileRename = function (name, newName, path, appType) {
    return new Promise((resolve, reject) => {
        var param = {
            Action: "rename",
            Name: name,
            NewName: newName,
            Path: path,
            appType: appType,
        };

        QB.CreateRequest(`${webApiRoot}File/FileManagerFileOperations`, "POST", param, true).then((result) => {
            if (typeof result.data == "string" || result.data.Type === "success") {
                resolve(result.data);
            } else {
                if (result.data.Message === "401 登录已过期，请重新登录") {
                    LoginOut();

                } else {
                    reject(result.data);
                }
            }
        }).catch((err) => {
            reject(err)
        });
    })
}

/**
 * 文件预览
 * @param {string} name 文件名称
 * @param {string} path 路径
 * @param {string} appType 应用类型
 */
QB.FileView = function (name, path, appType) {
    window.open(QB.GetEntFullPath(`DataCenter/AppManagement/content/file/FileView.html?appType=${appType}&path=${path}&name=${name}`))
}

/**
 * 转换字符串类型
 * @param input 需要转换的字符串
 * @returns {*|undefined|number|null|boolean}
 */
QB.parseStringToValue = function (input) {
    // 去掉首尾空格  
    const trimmedInput = input.trim();

    // 判断布尔值  
    if (trimmedInput.toLowerCase() === "true") return true;
    if (trimmedInput.toLowerCase() === "false") return false;

    // 判断 null  
    if (trimmedInput.toLowerCase() === "null") return null;

    // 判断 undefined  
    if (trimmedInput.toLowerCase() === "undefined") return undefined;

    // 判断数值  
    if (!isNaN(trimmedInput) && trimmedInput !== "") return Number(trimmedInput);

    // 判断 JSON 对象或数组  
    try {
        const parsed = JSON.parse(trimmedInput);
        if (typeof parsed === "object" || Array.isArray(parsed)) {
            return parsed;
        }
    } catch (error) {
        // 如果 JSON.parse 失败，说明不是有效的 JSON  
    }

    // 如果都不符合，返回原始字符串  
    return input;
}

/**
 * 根据参数返回类型
 * @param param
 * @returns {string|string}
 */
QB.getParamType = function (param) {
    if (Array.isArray(param)) {
        return "array"; // 判断是否为数组
    } else if (param instanceof Date) {
        return "datetime"; // 判断是否为日期对象
    } else if (typeof param === "string") {
        return "string"; // 判断是否为字符串
    } else if (typeof param === "boolean") {
        return "bool"; // 判断是否为布尔值
    } else if (typeof param === "number") {
        // 判断是否为数字
        return Number.isInteger(param) ? "int" : "double"; // 区分整数和浮点数
    } else if (typeof param === "object" && param !== null) {
        return "object"; // 判断是否为普通对象
    } else {
        return "unknown"; // 如果不属于上述类型，返回未知类型
    }
}

/**
 * 数组转换json对象
 * @param arrayData
 * @returns {{}}
 * @constructor
 */
QB.ArrayToJson = function (arrayData) {
    const result = {};
    const idMap = {}; // Used to store references to each node

    arrayData.forEach(item => {
        const { id, name, pid, type, value } = item;

        // Initialize the current node based on its type
        let currentNode;
        if (type === "array") {
            currentNode = [];
        } else if (type === "object") {
            currentNode = {};
        } else {
            currentNode = value || ""; // For primitive types, use the value directly
        }

        // Store the current node in the idMap
        idMap[id] = currentNode;

        // If the node has no parent, it's the root node
        if (!pid) {//(pid === null) {
            result[name] = currentNode;
        } else {
            // Find the parent node
            const parentNode = idMap[pid];

            if (Array.isArray(parentNode)) {
                // If the parent is an array, add the current node to it
                //parentNode[parseInt(name)] = currentNode;
                //数组直接push
                parentNode.push(currentNode);
            } else {
                // If the parent is an object, add the current node as a property
                parentNode[name] = currentNode;
            }
        }
    });

    return result;
}

/**
 * 转换json对象为对象数组
 * @param jsonObj
 * @param pid
 * @param path
 * @returns {*[]}
 * @constructor
 */
QB.TransformJsonToArray = function (jsonObj, pid = null, path = "") {
    const result = [];

    // Iterate over the JSON object
    for (const key in jsonObj) {
        if (jsonObj.hasOwnProperty(key)) {
            const item = jsonObj[key];

            // Determine the type of the current item
            const itemType = Array.isArray(item) ? "array" : typeof item;

            // Check if the item has children
            const hasChild = (itemType === "object" && item && Object.keys(item).length > 0) ||
                (itemType === "array" && item.length > 0);

            // Construct the current item's path
            const currentPath = path ? `${path}.${key}` : key;

            // Construct the current item
            const transformedItem = {
                id: currentPath,         // Use JMESPath-style path as id
                name: key,               // Property name
                pid: pid,                // Parent id
                type: itemType,          // Data type of the current item
                haschild: hasChild,      // Whether the item has children
                value: itemType === "object" || itemType === "array" ? null : item // Record the value for non-object/array types
            };

            result.push(transformedItem);

            // If the current item is an object or array, recursively process it
            if (itemType === "object") {
                result.push(...QB.TransformJsonToArray(item, currentPath, currentPath));
            } else if (itemType === "array") {
                // Iterate over each item in the array
                item.forEach((arrayItem, index) => {
                    const arrayItemType = Array.isArray(arrayItem) ? "array" : typeof arrayItem;
                    const arrayHasChild = (arrayItemType === "object" && Object.keys(arrayItem).length > 0) ||
                        (arrayItemType === "array" && arrayItem.length > 0);

                    const arrayPath = `${currentPath}[${index}]`;

                    const arrayTransformedItem = {
                        id: arrayPath,         // Path of the array item as id
                        name: `${index}`,      // Array item's name is its index
                        pid: currentPath,      // Parent id is the array's path
                        type: arrayItemType,   // Data type of the array item
                        haschild: arrayHasChild, // Whether the array item has children
                        value: arrayItemType === "object" || arrayItemType === "array" ? null : arrayItem // Record the value
                    };

                    result.push(arrayTransformedItem);

                    // Recursively process objects or arrays within the array
                    if (arrayItemType === "object" || arrayItemType === "array") {
                        result.push(...QB.TransformJsonToArray(arrayItem, arrayPath, arrayPath));
                    }
                });
            }
        }
    }

    return result;
}

QB.GenerateGUID = function () {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        const r = (Math.random() * 16) | 0; // 生成随机数  
        const v = c === 'x' ? r : (r & 0x3) | 0x8; // 确保符合 UUID v4 标准  
        return v.toString(16); // 转换为16进制  
    });
}




/*****流程相关 */
/**
    * 启动流程
    * @param {string} flowid
    * @param {string} formKey
    * @param {string} busRowid
    * @param {string} pageParams 页面参数,x=?&y=?...
    */
QB.StartFlow = function (flowid, formKey, busRowid, pageParams) {
    return new Promise((resolve, reject) => {
        DLG.Show(1200, 800, `DataCenter/Flow_RunDesign.html?flowobjid=&stepid=&flowid=${flowid}&formkey=${formKey}&busrowid=${busRowid}&pageParam=${encodeURIComponent(pageParams)}`, "启动流程", (result) => {
            resolve(result);
        });
    })
}

/**
    * 流程撤回
    * @param {string} wfid
    * @param {string} curStepId
    */
QB.RevokeFlow = function (wfid, curStepId) {
    return new Promise((resolve, reject) => {
        QB.CreateRequest(`${webApiRoot}Flow/FlowRevoke?curStepID=${curStepId}&wfid=${wfid}`, "POST", null, true, "json").then((result) => {
            const jsonData = result.data;
            resolve(jsonData);
        });
    })
}

/**
    * 流程退回
    * @param {string} wfid
    * @param {string} curStepId
    * @param {string} idea
    */
QB.ReturnFlow = function (wfid, curStepId, idea) {
    return new Promise((resolve, reject) => {
        let paramObj = {
            curStepId: curStepId,
            flowObjId: wfid,
            Idea: idea
        };
        QB.CreateRequest(`${webApiRoot}Flow/FlowBack`, "POST", paramObj, true, "json").then((result) => {
            const jsonData = result.data;
            resolve(jsonData);
        });
    })
}

/**
 * 流程强退（调度）
 * @param {string} wfid
 * @param {string} nodeId
 * @param {string} stepId
 * @param {string} idea
 * @returns
 */
QB.FlowForceBack = function (wfid, nodeId, stepId, idea) {
    return new Promise((resolve, reject) => {
        let paramObj = {
            curStepId: stepId,
            nodeId: nodeId,
            flowObjId: wfid,
            Idea: idea
        };
        QB.CreateRequest(`${webApiRoot}Flow/FlowForceBack`, "POST", paramObj, true, "json").then((result) => {
            const jsonData = result.data;
            resolve(jsonData);
        });
    })
}

///**
//    * 流程结束
//    * @param {string} wfid
//    */
//var EndFlow = (wfid) => {
//    return new Promise((resolve, reject) => {
//        ExecRequest(`${webApiRoot}`, "POST", null, (result) => {
//            resolve(result);
//        });
//    })
//}


/**
    * 流程追加办理人
    * @param {string} stepid
    * @param {string} wfid
    * @param {Array} userList
    */
QB.AddFlowReceiveUser = function (stepid, wfid, userList) {
    return new Promise((resolve, reject) => {
        QB.CreateRequest(`${webApiRoot}Flow/FlowAddReceiveUser?stepId=${stepid}&wfId=${wfid}`, "POST", userList, true, "json").then((result) => {
            const jsonData = result.data;
            resolve(jsonData);
        });
    })
}

/**
    * 获取流程候选人信息
    * @param {any} nodeid
    * @param {any} upNodeId
    * @param {any} wfid
    */
QB.GetFlowCandidate = function (nodeid, upNodeId, wfid) {
    return new Promise((resolve, reject) => {
        QB.CreateRequest(`${webApiRoot}Flow/GetExeUsers?nodeId=${nodeid}&upNodeId=${upNodeId}&wfId=${wfid}`, "GET", null, true, "json").then((result) => {
            const jsonData = result.data;
            resolve(jsonData);
        });
    })
}

/**
    * 流程驳回到起草人
    * @param {string} wfid
    * @param {string} curStepId
    * @param {string} idea
    */
QB.FlowToCreator = function (wfid, curStepId, idea) {
    return new Promise((resolve, reject) => {
        let paramObj = {
            curStepId: curStepId,
            flowObjId: wfid,
            Idea: idea
        };
        QB.CreateRequest(`${webApiRoot}Flow/FlowToCreator`, "POST", paramObj, true, "json").then((result) => {
            const jsonData = result.data;
            resolve(jsonData);
        });
    });
}



QB.SetParentsStyle = function (startElement, targetSelector, styleName, styleValue, isImportant) {
    let currentElement = startElement;

    while (currentElement) {
        // 设置当前元素的样式
        if (isImportant) {
            currentElement.style.setProperty(styleName, styleValue, "important");
        }
        else {
            currentElement.style.setProperty(styleName, styleValue);
        }

        // 检查是否匹配目标元素
        if (targetSelector.startsWith('#') && currentElement.id === targetSelector.slice(1)) {
            // 如果是 id 选择器（以 # 开头），匹配 id
            break;
        } else if (targetSelector.startsWith('.') && currentElement.classList.contains(targetSelector.slice(1))) {
            // 如果是 class 选择器（以 . 开头），匹配 class
            break;
        }

        // 向上移动到父节点
        currentElement = currentElement.parentElement;
    }
};


QB.formatDateToISOStringWithoutZ = function (dataObj) {

    function formatDateToISOStringWithoutZ(date) {
        return date.toISOString().replace('Z', '');
    }

    // 仅判断：字符串是日期格式 且 含有 Z/z，然后去掉其中的 Z/z
    // 支持常见 ISO 格式，如：2024-08-19T12:34:56.789Z / 2024-08-19T12:34:56Z
    // 注：只去掉 'Z'/'z'，不改动其他内容
    const isoDateWithZRegex =
        /^\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?[Zz]$/;

    function stripTrailingZFromDateString(str) {
        if (typeof str !== 'string') return str;
        // 只在看起来是 ISO 日期且以 Z/z 结尾时处理
        if (isoDateWithZRegex.test(str)) {
            return str.slice(0, -1); // 去掉末尾 Z/z
        }
        return str;
    }

    function recursivelyFormatDates(obj) {
        if (obj instanceof Date) {
            // 如果是 Date 类型，格式化为字符串
            return formatDateToISOStringWithoutZ(obj);
        } else if (Array.isArray(obj)) {
            // 如果是数组，递归处理每个元素
            return obj.map(item => recursivelyFormatDates(item));
        } else if (obj !== null && typeof obj === 'object') {
            // 如果是对象，递归处理每个键值对
            let formattedObj = {};
            for (let key in obj) {
                if (obj.hasOwnProperty(key)) {
                    formattedObj[key] = recursivelyFormatDates(obj[key]);
                }
            }
            return formattedObj;
        } else if (typeof obj === 'string') {
            // 仅对字符串日期且带 Z/z 的进行去除
            return stripTrailingZFromDateString(obj);
        }
        // 如果是其他类型，直接返回
        return obj;
    }

    return recursivelyFormatDates(dataObj);
}



//ctrl+m打开表单编辑页面
window.addEventListener("keydown", function (ev) {
    const oEvent = ev || window.event;
    if (oEvent.ctrlKey && oEvent.keyCode === 77) {
        QB.OpenFormEdit(oEvent);
    }
});

//ctrl+b打开bug管理页面
window.addEventListener("keydown", (ev) => {
    const oEvent = ev || window.event;
    if (oEvent.ctrlKey && oEvent.keyCode === 66) {
        QB.OpenBugManage();
    }
});

/**
 * 日期格式化
 * @param {string} fmt 字符串格式：yyyy-MM-dd
 */
Date.prototype.HtFormat = function (fmt) { //例:(new Date()).HtFormat('yyyy-MM-dd hh:mm:ss')
    const o = {
        "M+": this.getMonth() + 1, //月份
        "d+": this.getDate(), //日
        "h+": this.getHours(), //小时
        "m+": this.getMinutes(), //分
        "s+": this.getSeconds(), //秒
        "q+": Math.floor((this.getMonth() + 3) / 3), //季度
        "S": this.getMilliseconds() //毫秒
    };
    if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
    for (let k in o)
        if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
    return fmt;
}